/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
 
This file is part of Quake III Arena source code.
 
Quake III Arena source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
 
Quake III Arena source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with Foobar; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/
// sv_rankings.c -- global rankings interface

#include "server.h"
#include "..\rankings\1.0\gr\grapi.h"
#include "..\rankings\1.0\gr\grlog.h"

typedef struct
{
    GR_CONTEXT		context;
    uint64_t        game_id;
    uint64_t		match;
    uint64_t		player_id;
    GR_PLAYER_TOKEN     token;
    grank_status_t	grank_status;
    grank_status_t	final_status;	// status to set after cleanup
    uint32_t        grank;          // global rank
    char			name[32];
}
ranked_player_t;

static int				s_rankings_contexts = 0;
static qboolean			s_rankings_active = qfalse;
static GR_CONTEXT		s_server_context = 0;
static uint64_t			s_server_match = 0;
static char*			s_rankings_game_key = NULL;
static uint64_t			s_rankings_game_id = 0;
static ranked_player_t*	s_ranked_players = NULL;
static qboolean			s_server_quitting = qfalse;
static const char		s_ascii_encoding[] =
    "0123456789abcdef"
    "ghijklmnopqrstuv"
    "wxyzABCDEFGHIJKL"
    "MNOPQRSTUVWXYZ[]";

// private functions
static void		SV_RankNewGameCBF( GR_NEWGAME* gr_newgame, void* cbf_arg );
static void		SV_RankUserCBF( GR_LOGIN* gr_login, void* cbf_arg );
static void		SV_RankJoinGameCBF( GR_JOINGAME* gr_joingame, void* cbf_arg );
static void		SV_RankSendReportsCBF( GR_STATUS* gr_status, void* cbf_arg );
static void		SV_RankCleanupCBF( GR_STATUS* gr_status, void* cbf_arg );
static void		SV_RankCloseContext( ranked_player_t* ranked_player );
static int		SV_RankAsciiEncode( char* dest, const unsigned char* src,
                                int src_len );
static int		SV_RankAsciiDecode( unsigned char* dest, const char* src,
                                int src_len );
static void		SV_RankEncodeGameID( uint64_t game_id, char* result,
                                  int len );
static uint64_t	SV_RankDecodePlayerID( const char* string );
static void		SV_RankDecodePlayerKey( const char* string, GR_PLAYER_TOKEN key );
static char*	SV_RankStatusString( GR_STATUS status );
static void		SV_RankError( const char* fmt, ... );
static char     SV_RankGameKey[64];

/*
================
SV_RankBegin
================
*/
void SV_RankBegin( char *gamekey )
{
    GR_INIT		init;
    GR_STATUS	status;

    assert( s_rankings_contexts == 0 );
    assert( !s_rankings_active );
    assert( s_ranked_players == NULL );

    if( sv_enableRankings->integer == 0 || Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER )
    {
        s_rankings_active = qfalse;
        if( sv_rankingsActive->integer == 1 )
        {
            Cvar_Set( "sv_rankingsActive", "0" );
        }
        return;
    }

    // only allow official game key on pure servers
    if( strcmp(gamekey, GR_GAMEKEY) == 0 )
    {
        /*
        		if( Cvar_VariableValue("sv_pure") != 1 )
        		{
        			Cvar_Set( "sv_enableRankings", "0" );
        			return;
        		}
        */

        // substitute game-specific game key
        switch( (int)Cvar_VariableValue("g_gametype") )
        {
        case GT_FFA:
            gamekey = "Q3 Free For All";
            break;
        case GT_TOURNAMENT:
            gamekey = "Q3 Tournament";
            break;
        case GT_TEAM:
            gamekey = "Q3 Team Deathmatch";
            break;
        case GT_CTF:
            gamekey = "Q3 Capture the Flag";
            break;
        case GT_1FCTF:
            gamekey = "Q3 One Flag CTF";
            break;
        case GT_OBELISK:
            gamekey = "Q3 Overload";
            break;
        case GT_HARVESTER:
            gamekey = "Q3 Harvester";
            break;
        default:
            break;
        }
    }
    s_rankings_game_key = gamekey;

    // initialize rankings
    GRankLogLevel( GRLOG_OFF );
    memset(SV_RankGameKey,0,sizeof(SV_RankGameKey));
    strncpy(SV_RankGameKey,gamekey,sizeof(SV_RankGameKey)-1);
    init = GRankInit( 1, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END );
    s_server_context = init.context;
    s_rankings_contexts++;
    Com_DPrintf( "SV_RankBegin(); GR_GAMEKEY is %s\n", gamekey );
    Com_DPrintf( "SV_RankBegin(); s_rankings_contexts=%d\n",s_rankings_contexts );
    Com_DPrintf( "SV_RankBegin(); s_server_context=%d\n",init.context );

    // new game
    if(!strlen(Cvar_VariableString( "sv_leagueName" )))
    {
        status = GRankNewGameAsync
                 (
                     s_server_context,
                     SV_RankNewGameCBF,
                     NULL,
                     GR_OPT_LEAGUENAME,
                     (void*)(Cvar_VariableString( "sv_leagueName" )),
                     GR_OPT_END
                 );
    }
    else
    {
        status = GRankNewGameAsync
                 (
                     s_server_context,
                     SV_RankNewGameCBF,
                     NULL,
                     GR_OPT_END
                 );
    }

    if( status != GR_STATUS_PENDING )
    {
        SV_RankError( "SV_RankBegin: Expected GR_STATUS_PENDING, got %s",
                      SV_RankStatusString( status ) );
        return;
    }

    // logging
    if( com_developer->value )
    {
        GRankLogLevel( GRLOG_TRACE );
    }

    // allocate rankings info for each player
    s_ranked_players = Z_Malloc( sv_maxclients->value *
                                 sizeof(ranked_player_t) );
    memset( (void*)s_ranked_players, 0 ,sv_maxclients->value
            * sizeof(ranked_player_t));
}

/*
================
SV_RankEnd
================
*/
void SV_RankEnd( void )
{
    GR_STATUS	status;
    int			i;

    Com_DPrintf( "SV_RankEnd();\n" );

    if( !s_rankings_active )
    {
        // cleanup after error during game
        if( s_ranked_players != NULL )
        {
            for( i = 0; i < sv_maxclients->value; i++ )
            {
                if( s_ranked_players[i].context != 0 )
                {
                    SV_RankCloseContext( &(s_ranked_players[i]) );
                }
            }
        }
        if( s_server_context != 0 )
        {
            SV_RankCloseContext( NULL );
        }

        return;
    }

    for( i = 0; i < sv_maxclients->value; i++ )
    {
        if( s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE )
        {
            SV_RankUserLogout( i );
            Com_DPrintf( "SV_RankEnd: SV_RankUserLogout %d\n",i );
        }
    }

    assert( s_server_context != 0 );

    // send match reports, proceed to SV_RankSendReportsCBF
    status = GRankSendReportsAsync
             (
                 s_server_context,
                 0,
                 SV_RankSendReportsCBF,
                 NULL,
                 GR_OPT_END
             );

    if( status != GR_STATUS_PENDING )
    {
        SV_RankError( "SV_RankEnd: Expected GR_STATUS_PENDING, got %s",
                      SV_RankStatusString( status ) );
    }

    s_rankings_active = qfalse;
    Cvar_Set( "sv_rankingsActive", "0" );
}

/*
================
SV_RankPoll
================
*/
void SV_RankPoll( void )
{
    GRankPoll();
}

/*
================
SV_RankCheckInit
================
*/
qboolean SV_RankCheckInit( void )
{
    return (s_rankings_contexts > 0);
}

/*
================
SV_RankActive
================
*/
qboolean SV_RankActive( void )
{
    return s_rankings_active;
}

/*
=================
SV_RankUserStatus
=================
*/
grank_status_t SV_RankUserStatus( int index )
{
    if( !s_rankings_active )
    {
        return GR_STATUS_ERROR;
    }

    assert( s_ranked_players != NULL );
    assert( index >= 0 );
    assert( index < sv_maxclients->value );

    return s_ranked_players[index].grank_status;
}

/*
================
SV_RankUserGRank
================
*/
int SV_RankUserGrank( int index )
{
    if( !s_rankings_active )
    {
        return 0;
    }

    assert( s_ranked_players != NULL );
    assert( index >= 0 );
    assert( index < sv_maxclients->value );

    return s_ranked_players[index].grank;
}

/*
================
SV_RankUserReset
================
*/
void SV_RankUserReset( int index )
{
    if( !s_rankings_active )
    {
        return;
    }

    assert( s_ranked_players != NULL );
    assert( index >= 0 );
    assert( index < sv_maxclients->value );

    switch( s_ranked_players[index].grank_status )
    {
    case QGR_STATUS_SPECTATOR:
    case QGR_STATUS_NO_USER:
    case QGR_STATUS_BAD_PASSWORD:
    case QGR_STATUS_USER_EXISTS:
    case QGR_STATUS_NO_MEMBERSHIP:
    case QGR_STATUS_TIMEOUT:
    case QGR_STATUS_ERROR:
        s_ranked_players[index].grank_status = QGR_STATUS_NEW;
        break;
    default:
        break;
    }
}

/*
================
SV_RankUserSpectate
================
*/
void SV_RankUserSpectate( int index )
{
    if( !s_rankings_active )
    {
        return;
    }

    assert( s_ranked_players != NULL );
    assert( index >= 0 );
    assert( index < sv_maxclients->value );

    // GRANK_FIXME - check current status?
    s_ranked_players[index].grank_status = QGR_STATUS_SPECTATOR;
}

/*
================
SV_RankUserCreate
================
*/
void SV_RankUserCreate( int index, char* username, char* password,
                        char* email )
{
    GR_INIT		init;
    GR_STATUS	status;

    assert( index >= 0 );
    assert( index < sv_maxclients->value );
    assert( username != NULL );
    assert( password != NULL );
    assert( email != NULL );
    assert( s_ranked_players );
    assert( s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE );

    Com_DPrintf( "SV_RankUserCreate( %d, %s, \"****\", %s );\n", index,
                 username, email );

    if( !s_rankings_active )
    {
        Com_DPrintf( "SV_RankUserCreate: Not ready to create\n" );
        s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
        return;
    }

    if( s_ranked_players[index].grank_status == QGR_STATUS_ACTIVE )
    {
        Com_DPrintf( "SV_RankUserCreate: Got Create from active player\n" );
        return;
    }

    // get a separate context for the new user
    init = GRankInit( 0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END );
    s_ranked_players[index].context = init.context;
    s_rankings_contexts++;
    Com_DPrintf( "SV_RankUserCreate(); s_rankings_contexts=%d\n",s_rankings_contexts );
    Com_DPrintf( "SV_RankUserCreate(); s_ranked_players[%d].context=%d\n",index,init.context );

    // attempt to create a new account, proceed to SV_RankUserCBF
    status = GRankUserCreateAsync
             (
                 s_ranked_players[index].context,
                 username,
                 password,
                 email,
                 SV_RankUserCBF,
                 (void*)&s_ranked_players[index],
                 GR_OPT_END
             );

    if( status == GR_STATUS_PENDING )
    {
        s_ranked_players[index].grank_status = QGR_STATUS_PENDING;
        s_ranked_players[index].final_status = QGR_STATUS_NEW;
    }
    else
    {
        SV_RankError( "SV_RankUserCreate: Expected GR_STATUS_PENDING, got %s",
                      SV_RankStatusString( status ) );
    }
}

/*
================
SV_RankUserLogin
================
*/
void SV_RankUserLogin( int index, char* username, char* password )
{
    GR_INIT		init;
    GR_STATUS	status;

    assert( index >= 0 );
    assert( index < sv_maxclients->value );
    assert( username != NULL );
    assert( password != NULL );
    assert( s_ranked_players );
    assert( s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE );

    Com_DPrintf( "SV_RankUserLogin( %d, %s, \"****\" );\n", index, username );

    if( !s_rankings_active )
    {
        Com_DPrintf( "SV_RankUserLogin: Not ready for login\n" );
        s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
        return;
    }

    if( s_ranked_players[index].grank_status == QGR_STATUS_ACTIVE )
    {
        Com_DPrintf( "SV_RankUserLogin: Got Login from active player\n" );
        return;
    }

    // get a separate context for the new user
    init = GRankInit( 0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END );
    s_ranked_players[index].context = init.context;
    s_rankings_contexts++;
    Com_DPrintf( "SV_RankUserLogin(); s_rankings_contexts=%d\n",s_rankings_contexts );
    Com_DPrintf( "SV_RankUserLogin(); s_ranked_players[%d].context=%d\n",index,init.context );

    // login user, proceed to SV_RankUserCBF
    status = GRankUserLoginAsync
             (
                 s_ranked_players[index].context,
                 username,
                 password,
                 SV_RankUserCBF,
                 (void*)&s_ranked_players[index],
                 GR_OPT_END
             );

    if( status == GR_STATUS_PENDING )
    {
        s_ranked_players[index].grank_status = QGR_STATUS_PENDING;
        s_ranked_players[index].final_status = QGR_STATUS_NEW;
    }
    else
    {
        SV_RankError( "SV_RankUserLogin: Expected GR_STATUS_PENDING, got %s",
                      SV_RankStatusString( status )  );
    }
}

/*
===================
SV_RankUserValidate
===================
*/
qboolean SV_RankUserValidate( int index, const char* player_id, const char* key, int token_len, int rank, char* name )
{
    GR_INIT		init;
    GR_STATUS status;
    qboolean rVal;
    ranked_player_t* ranked_player;
    int i;

    assert( s_ranked_players );
    assert( s_ranked_players[index].grank_status != QGR_STATUS_ACTIVE );

    rVal = qfalse;

    if( !s_rankings_active )
    {
        Com_DPrintf( "SV_RankUserValidate: Not ready to validate\n" );
        s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
        return rVal;
    }

    ranked_player = &(s_ranked_players[index]);

    if ( (player_id != NULL) && (key != NULL))
    {
        // the real player_id and key is set when SV_RankJoinGameCBF
        // is called we do this so that SV_RankUserValidate
        // can be shared by both server side login and client side login

        // for client side logined in players
        // server is creating GR_OPT_PLAYERCONTEXT
        init = GRankInit( 0, SV_RankGameKey, GR_OPT_POLL, GR_OPT_END );
        ranked_player->context   = init.context;
        s_rankings_contexts++;
        Com_DPrintf( "SV_RankUserValidate(); s_rankings_contexts=%d\n",s_rankings_contexts );
        Com_DPrintf( "SV_RankUserValidate(); s_ranked_players[%d].context=%d\n",index,init.context );

        // uudecode player id and player token
        ranked_player->player_id = SV_RankDecodePlayerID(player_id);
        Com_DPrintf( "SV_RankUserValidate(); ranked_player->player_id =%u\n", (uint32_t)ranked_player->player_id );
        SV_RankDecodePlayerKey(key, ranked_player->token);

        // save name and check for duplicates
        Q_strncpyz( ranked_player->name, name, sizeof(ranked_player->name) );
        for( i = 0; i < sv_maxclients->value; i++ )
        {
            if( (i != index) && (s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE) &&
                    (strcmp( s_ranked_players[i].name, name ) == 0) )
            {
                Com_DPrintf( "SV_RankUserValidate: Duplicate login\n" );
                ranked_player->grank_status = QGR_STATUS_NO_USER;
                ranked_player->final_status = QGR_STATUS_NEW;
                ranked_player->grank = 0;
                return qfalse;
            }
        }

        // then validate
        status  = GRankPlayerValidate(
                      s_server_context,
                      ranked_player->player_id,
                      ranked_player->token,
                      token_len,
                      GR_OPT_PLAYERCONTEXT,
                      ranked_player->context,
                      GR_OPT_END);
    }
    else
    {
        // make server side login (bots) happy
        status = GR_STATUS_OK;
    }

    if (status == GR_STATUS_OK)
    {
        ranked_player->grank_status = QGR_STATUS_ACTIVE;
        ranked_player->final_status = QGR_STATUS_NEW;
        ranked_player->grank = rank;
        rVal = qtrue;
    }
    else if (status == GR_STATUS_INVALIDUSER)
    {
        ranked_player->grank_status = QGR_STATUS_INVALIDUSER;
        ranked_player->final_status = QGR_STATUS_NEW;
        ranked_player->grank = 0;
        rVal = qfalse;
    }
    else
    {
        SV_RankError( "SV_RankUserValidate: Unexpected status %s",
                      SV_RankStatusString( status ) );
        s_ranked_players[index].grank_status = QGR_STATUS_ERROR;
        ranked_player->grank = 0;
    }

    return rVal;
}

/*
================
SV_RankUserLogout
================
*/
void SV_RankUserLogout( int index )
{
    GR_STATUS	status;
    GR_STATUS	cleanup_status;

    if( !s_rankings_active )
    {
        return;
    }

    assert( index >= 0 );
    assert( index < sv_maxclients->value );
    assert( s_ranked_players );

    if( s_ranked_players[index].context == 0 )
    {
        return;
    }

    Com_DPrintf( "SV_RankUserLogout( %d );\n", index );

    // masqueraded player may not be active yet, if they fail validation,
    // but still they have a context needs to be cleaned
    // what matters is the s_ranked_players[index].context

    // send reports, proceed to SV_RankSendReportsCBF
    status = GRankSendReportsAsync
             (
                 s_ranked_players[index].context,
                 0,
                 SV_RankSendReportsCBF,
                 (void*)&s_ranked_players[index],
                 GR_OPT_END
             );

    if( status == GR_STATUS_PENDING )
    {
        s_ranked_players[index].grank_status = QGR_STATUS_PENDING;
        s_ranked_players[index].final_status = QGR_STATUS_NEW;
    }
    else
    {
        SV_RankError( "SV_RankUserLogout: Expected GR_STATUS_PENDING, got %s",
                      SV_RankStatusString( status ) );

        cleanup_status = GRankCleanupAsync
                         (
                             s_ranked_players[index].context,
                             0,
                             SV_RankCleanupCBF,
                             (void*)&s_ranked_players[index],
                             GR_OPT_END
                         );

        if( cleanup_status != GR_STATUS_PENDING )
        {
            SV_RankError( "SV_RankUserLogout: Expected "
                          "GR_STATUS_PENDING from GRankCleanupAsync, got %s",
                          SV_RankStatusString( cleanup_status ) );
            SV_RankCloseContext( &(s_ranked_players[index]) );
        }
    }
}

/*
================
SV_RankReportInt
================
*/
void SV_RankReportInt( int index1, int index2, int key, int value,
                       qboolean accum )
{
    GR_STATUS	status;
    GR_CONTEXT	context;
    uint64_t	match;
    uint64_t	user1;
    uint64_t	user2;
    int			opt_accum;

    if( !s_rankings_active )
    {
        return;
    }

    assert( index1 >= -1 );
    assert( index1 < sv_maxclients->value );
    assert( index2 >= -1 );
    assert( index2 < sv_maxclients->value );
    assert( s_ranked_players );

//	Com_DPrintf( "SV_RankReportInt( %d, %d, %d, %d, %d );\n", index1, index2,
//		key, value, accum );

    // get context, match, and player_id for player index1
    if( index1 == -1 )
    {
        context = s_server_context;
        match = s_server_match;
        user1 = 0;
    }
    else
    {
        if( s_ranked_players[index1].grank_status != QGR_STATUS_ACTIVE )
        {
            Com_DPrintf( "SV_RankReportInt: Expecting QGR_STATUS_ACTIVE"
                         " Got Unexpected status %d for player %d\n",
                         s_ranked_players[index1].grank_status, index1 );
            return;
        }

        context = s_ranked_players[index1].context;
        match = s_ranked_players[index1].match;
        user1 = s_ranked_players[index1].player_id;
    }

    // get player_id for player index2
    if( index2 == -1 )
    {
        user2 = 0;
    }
    else
    {
        if( s_ranked_players[index2].grank_status != QGR_STATUS_ACTIVE )
        {
            Com_DPrintf( "SV_RankReportInt: Expecting QGR_STATUS_ACTIVE"
                         " Got Unexpected status %d for player %d\n",
                         s_ranked_players[index2].grank_status, index2 );
            return;
        }

        user2 = s_ranked_players[index2].player_id;
    }

    opt_accum = accum ? GR_OPT_ACCUM : GR_OPT_END;

    status = GRankReportInt
             (
                 context,
                 match,
                 user1,
                 user2,
                 key,
                 value,
                 opt_accum,
                 GR_OPT_END
             );

    if( status != GR_STATUS_OK )
    {
        SV_RankError( "SV_RankReportInt: Unexpected status %s",
                      SV_RankStatusString( status ) );
    }

    if( user2 != 0 )
    {
        context = s_ranked_players[index2].context;
        match   = s_ranked_players[index2].match;

        status = GRankReportInt
                 (
                     context,
                     match,
                     user1,
                     user2,
                     key,
                     value,
                     opt_accum,
                     GR_OPT_END
                 );

        if( status != GR_STATUS_OK )
        {
            SV_RankError( "SV_RankReportInt: Unexpected status %s",
                          SV_RankStatusString( status ) );
        }
    }
}

/*
================
SV_RankReportStr
================
*/
void SV_RankReportStr( int index1, int index2, int key, char* value )
{
    GR_STATUS	status;
    GR_CONTEXT	context;
    uint64_t	match;
    uint64_t	user1;
    uint64_t	user2;

    if( !s_rankings_active )
    {
        return;
    }

    assert( index1 >= -1 );
    assert( index1 < sv_maxclients->value );
    assert( index2 >= -1 );
    assert( index2 < sv_maxclients->value );
    assert( s_ranked_players );

//	Com_DPrintf( "SV_RankReportStr( %d, %d, %d, \"%s\" );\n", index1, index2,
//		key, value );

    // get context, match, and player_id for player index1
    if( index1 == -1 )
    {
        context = s_server_context;
        match = s_server_match;
        user1 = 0;
    }
    else
    {
        if( s_ranked_players[index1].grank_status != QGR_STATUS_ACTIVE )
        {
            Com_DPrintf( "SV_RankReportStr: Unexpected status %d\n",
                         s_ranked_players[index1].grank_status );
            return;
        }

        context = s_ranked_players[index1].context;
        match = s_ranked_players[index1].match;
        user1 = s_ranked_players[index1].player_id;
    }

    // get player_id for player index2
    if( index2 == -1 )
    {
        user2 = 0;
    }
    else
    {
        if( s_ranked_players[index2].grank_status != QGR_STATUS_ACTIVE )
        {
            Com_DPrintf( "SV_RankReportStr: Unexpected status %d\n",
                         s_ranked_players[index2].grank_status );
            return;
        }

        user2 = s_ranked_players[index2].player_id;
    }

    status = GRankReportStr
             (
                 context,
                 match,
                 user1,
                 user2,
                 key,
                 value,
                 GR_OPT_END
             );

    if( status != GR_STATUS_OK )
    {
        SV_RankError( "SV_RankReportStr: Unexpected status %s",
                      SV_RankStatusString( status ) );
    }

    if( user2 != 0 )
    {
        context = s_ranked_players[index2].context;
        match = s_ranked_players[index2].match;

        status = GRankReportStr
                 (
                     context,
                     match,
                     user1,
                     user2,
                     key,
                     value,
                     GR_OPT_END
                 );

        if( status != GR_STATUS_OK )
        {
            SV_RankError( "SV_RankReportInt: Unexpected status %s",
                          SV_RankStatusString( status ) );
        }
    }
}

/*
================
SV_RankQuit
================
*/
void SV_RankQuit( void )
{
    int	i;
    int j = 0;
    // yuck

    while( s_rankings_contexts > 1 )
    {
        assert(s_ranked_players);
        if( s_ranked_players != NULL )
        {
            for( i = 0; i < sv_maxclients->value; i++ )
            {
                // check for players that weren't yet active in SV_RankEnd
                if( s_ranked_players[i].grank_status == QGR_STATUS_ACTIVE )
                {
                    SV_RankUserLogout( i );
                    Com_DPrintf( "SV_RankQuit: SV_RankUserLogout %d\n",i );
                }
                else
                {
                    if( s_ranked_players[i].context )
                    {
                        GR_STATUS cleanup_status;
                        cleanup_status = GRankCleanupAsync
                                         (
                                             s_ranked_players[i].context,
                                             0,
                                             SV_RankCleanupCBF,
                                             (void*)&(s_ranked_players[i]),
                                             GR_OPT_END
                                         );

                        if( cleanup_status != GR_STATUS_PENDING )
                        {
                            SV_RankError( "SV_RankQuit: Expected "
                                          "GR_STATUS_PENDING from GRankCleanupAsync, got %s",
                                          SV_RankStatusString( cleanup_status ) );
                        }
                    }
                }
            }
        }
        SV_RankPoll();

        // should've finished by now
        assert( (j++) < 68 );
    }
}

/*
==============================================================================
 
Private Functions
 
==============================================================================
*/

/*
=================
SV_RankNewGameCBF
=================
*/
static void SV_RankNewGameCBF( GR_NEWGAME* gr_newgame, void* cbf_arg )
{
    GR_MATCH	match;
    int			i;

    assert( gr_newgame != NULL );
    assert( cbf_arg == NULL );

    Com_DPrintf( "SV_RankNewGameCBF( %08X, %08X );\n", gr_newgame, cbf_arg );

    if( gr_newgame->status == GR_STATUS_OK )
    {
        char info[MAX_INFO_STRING];
        char gameid[sizeof(s_ranked_players[i].game_id) * 4 / 3 + 2];

        // save game id
        s_rankings_game_id = gr_newgame->game_id;

        // encode gameid
        memset(gameid,0,sizeof(gameid));
        SV_RankEncodeGameID(s_rankings_game_id,gameid,sizeof(gameid));

        // set CS_GRANK rankingsGameID to pass to client
        memset(info,0,sizeof(info));
        Info_SetValueForKey( info, "rankingsGameKey", s_rankings_game_key );
        Info_SetValueForKey( info, "rankingsGameID", gameid );
        SV_SetConfigstring( CS_GRANK, info );

        // initialize client status
        for( i = 0; i < sv_maxclients->value; i++ )
            s_ranked_players[i].grank_status = QGR_STATUS_NEW;

        // start new match
        match = GRankStartMatch( s_server_context );
        s_server_match = match.match;

        // ready to go
        s_rankings_active = qtrue;
        Cvar_Set( "sv_rankingsActive", "1" );

    }
    else if( gr_newgame->status == GR_STATUS_BADLEAGUE )
    {
        SV_RankError( "SV_RankNewGameCBF: Invalid League name\n" );
    }
    else
    {
        //GRank handle new game failure
        // force  SV_RankEnd() to run
        //SV_RankEnd();
        SV_RankError( "SV_RankNewGameCBF: Unexpected status %s",
                      SV_RankStatusString( gr_newgame->status ) );
    }
}

/*
================
SV_RankUserCBF
================
*/
static void SV_RankUserCBF( GR_LOGIN* gr_login, void* cbf_arg )
{
    ranked_player_t*	ranked_player;
    GR_STATUS			join_status;
    GR_STATUS			cleanup_status;

    assert( gr_login != NULL );
    assert( cbf_arg != NULL );

    Com_DPrintf( "SV_RankUserCBF( %08X, %08X );\n", gr_login, cbf_arg );

    ranked_player = (ranked_player_t*)cbf_arg;
    assert(ranked_player);
    assert( ranked_player->context );

    switch( gr_login->status )
    {
    case GR_STATUS_OK:
        // attempt to join the game, proceed to SV_RankJoinGameCBF
        join_status = GRankJoinGameAsync
                      (
                          ranked_player->context,
                          s_rankings_game_id,
                          SV_RankJoinGameCBF,
                          cbf_arg,
                          GR_OPT_END
                      );

        if( join_status != GR_STATUS_PENDING )
        {
            SV_RankError( "SV_RankUserCBF: Expected GR_STATUS_PENDING "
                          "from GRankJoinGameAsync, got %s",
                          SV_RankStatusString( join_status ) );
        }
        break;
    case GR_STATUS_NOUSER:
        Com_DPrintf( "SV_RankUserCBF: Got status %s\n",
                     SV_RankStatusString( gr_login->status ) );
        ranked_player->final_status = QGR_STATUS_NO_USER;
        break;
    case GR_STATUS_BADPASSWORD:
        Com_DPrintf( "SV_RankUserCBF: Got status %s\n",
                     SV_RankStatusString( gr_login->status ) );
        ranked_player->final_status = QGR_STATUS_BAD_PASSWORD;
        break;
    case GR_STATUS_TIMEOUT:
        Com_DPrintf( "SV_RankUserCBF: Got status %s\n",
                     SV_RankStatusString( gr_login->status ) );
        ranked_player->final_status = QGR_STATUS_TIMEOUT;
        break;
    default:
        Com_DPrintf( "SV_RankUserCBF: Unexpected status %s\n",
                     SV_RankStatusString( gr_login->status ) );
        ranked_player->final_status = QGR_STATUS_ERROR;
        break;
    }

    if( ranked_player->final_status != QGR_STATUS_NEW )
    {
        // login or create failed, so clean up before the next attempt
        cleanup_status = GRankCleanupAsync
                         (
                             ranked_player->context,
                             0,
                             SV_RankCleanupCBF,
                             (void*)ranked_player,
                             GR_OPT_END
                         );

        if( cleanup_status != GR_STATUS_PENDING )
        {
            SV_RankError( "SV_RankUserCBF: Expected GR_STATUS_PENDING "
                          "from GRankCleanupAsync, got %s",
                          SV_RankStatusString( cleanup_status ) );
            SV_RankCloseContext( ranked_player );
        }
    }
}

/*
================
SV_RankJoinGameCBF
================
*/
static void SV_RankJoinGameCBF( GR_JOINGAME* gr_joingame, void* cbf_arg )
{
    ranked_player_t*	ranked_player;
    GR_MATCH			match;
    GR_STATUS           cleanup_status;

    assert( gr_joingame != NULL );
    assert( cbf_arg != NULL );

    Com_DPrintf( "SV_RankJoinGameCBF( %08X, %08X );\n", gr_joingame, cbf_arg );

    ranked_player = (ranked_player_t*)cbf_arg;

    assert( ranked_player );
    assert( ranked_player->context != 0 );

    if( gr_joingame->status == GR_STATUS_OK )
    {
        int i;
        // save user id
        ranked_player->player_id = gr_joingame->player_id;
        memcpy(ranked_player->token,gr_joingame->token,
               sizeof(GR_PLAYER_TOKEN)) ;
        match = GRankStartMatch( ranked_player->context );
        ranked_player->match = match.match;
        ranked_player->grank = gr_joingame->rank;

        // find the index and call SV_RankUserValidate
        for (i=0;i<sv_maxclients->value;i++)
            if ( ranked_player == &s_ranked_players[i] )
                SV_RankUserValidate(i,NULL,NULL,0, gr_joingame->rank,ranked_player->name);
    }
    else
    {
        //GRand handle join game failure
        SV_RankError( "SV_RankJoinGameCBF: Unexpected status %s",
                      SV_RankStatusString( gr_joingame->status ) );

        cleanup_status = GRankCleanupAsync
                         (
                             ranked_player->context,
                             0,
                             SV_RankCleanupCBF,
                             cbf_arg,
                             GR_OPT_END
                         );

        if( cleanup_status != GR_STATUS_PENDING )
        {
            SV_RankError( "SV_RankJoinGameCBF: Expected "
                          "GR_STATUS_PENDING from GRankCleanupAsync, got %s",
                          SV_RankStatusString( cleanup_status ) );
            SV_RankCloseContext( ranked_player );
        }
    }
}

/*
================
SV_RankSendReportsCBF
================
*/
static void SV_RankSendReportsCBF( GR_STATUS* status, void* cbf_arg )
{
    ranked_player_t*	ranked_player;
    GR_CONTEXT			context;
    GR_STATUS			cleanup_status;

    assert( status != NULL );
    // NULL cbf_arg means server is sending match reports

    Com_DPrintf( "SV_RankSendReportsCBF( %08X, %08X );\n", status, cbf_arg );

    ranked_player = (ranked_player_t*)cbf_arg;
    if( ranked_player == NULL )
    {
        Com_DPrintf( "SV_RankSendReportsCBF: server\n" );
        context = s_server_context;
    }
    else
    {
        Com_DPrintf( "SV_RankSendReportsCBF: player\n" );
        context = ranked_player->context;
    }

    //assert( context != 0 );
    if( *status != GR_STATUS_OK )
    {
        SV_RankError( "SV_RankSendReportsCBF: Unexpected status %s",
                      SV_RankStatusString( *status ) );
    }

    if( context == 0 )
    {
        Com_DPrintf( "SV_RankSendReportsCBF: WARNING: context == 0" );
        SV_RankCloseContext( ranked_player );
    }
    else
    {
        cleanup_status = GRankCleanupAsync
                         (
                             context,
                             0,
                             SV_RankCleanupCBF,
                             cbf_arg,
                             GR_OPT_END
                         );

        if( cleanup_status != GR_STATUS_PENDING )
        {
            SV_RankError( "SV_RankSendReportsCBF: Expected "
                          "GR_STATUS_PENDING from GRankCleanupAsync, got %s",
                          SV_RankStatusString( cleanup_status ) );
            SV_RankCloseContext( ranked_player );
        }
    }
}

/*
================
SV_RankCleanupCBF
================
*/
static void SV_RankCleanupCBF( GR_STATUS* status, void* cbf_arg )
{
    ranked_player_t*	ranked_player;
    ranked_player = (ranked_player_t*)cbf_arg;

    assert( status != NULL );
    // NULL cbf_arg means server is cleaning up

    Com_DPrintf( "SV_RankCleanupCBF( %08X, %08X );\n", status, cbf_arg );

    if( *status != GR_STATUS_OK )
    {
        SV_RankError( "SV_RankCleanupCBF: Unexpected status %s",
                      SV_RankStatusString( *status ) );
    }

    SV_RankCloseContext( ranked_player );
}

/*
================
SV_RankCloseContext
================
*/
static void SV_RankCloseContext( ranked_player_t* ranked_player )
{
    if( ranked_player == NULL )
    {
        // server cleanup
        if( s_server_context == 0 )
        {
            return;
        }
        s_server_context = 0;
        s_server_match = 0;
    }
    else
    {
        // player cleanup
        if( s_ranked_players == NULL )
        {
            return;
        }
        if( ranked_player->context == 0 )
        {
            return;
        }
        ranked_player->context = 0;
        ranked_player->match = 0;
        ranked_player->player_id = 0;
        memset( ranked_player->token, 0, sizeof(GR_PLAYER_TOKEN) );
        ranked_player->grank_status = ranked_player->final_status;
        ranked_player->final_status = QGR_STATUS_NEW;
        ranked_player->name[0] = '\0';
    }

    assert( s_rankings_contexts > 0 );
    s_rankings_contexts--;
    Com_DPrintf( "SV_RankCloseContext: s_rankings_contexts = %d\n",
                 s_rankings_contexts );

    if( s_rankings_contexts == 0 )
    {
        GRankLogLevel( GRLOG_OFF );

        if( s_ranked_players != NULL )
        {
            Z_Free( s_ranked_players );
            s_ranked_players = NULL;
        }

        s_rankings_active = qfalse;
        Cvar_Set( "sv_rankingsActive", "0" );
    }
}

/*
================
SV_RankAsciiEncode
 
Encodes src_len bytes of binary data from the src buffer as ASCII text, 
using 6 bits per character. The result string is null-terminated and 
stored in the dest buffer.
 
The dest buffer must be at least (src_len * 4) / 3 + 2 bytes in length.
 
Returns the length of the result string, not including the null.
================
*/
static int SV_RankAsciiEncode( char* dest, const unsigned char* src,
                               int src_len )
{
    unsigned char	bin[3];
    unsigned char	txt[4];
    int				dest_len = 0;
    int				i;
    int				j;
    int				num_chars;

    assert( dest != NULL );
    assert( src != NULL );

    for( i = 0; i < src_len; i += 3 )
    {
        // read three bytes of input
        for( j = 0; j < 3; j++ )
        {
            bin[j] = (i + j < src_len) ? src[i + j] : 0;
        }

        // get four 6-bit values from three bytes
        txt[0] = bin[0] >> 2;
        txt[1] = ((bin[0] << 4) | (bin[1] >> 4)) & 63;
        txt[2] = ((bin[1] << 2) | (bin[2] >> 6)) & 63;
        txt[3] = bin[2] & 63;

        // store ASCII encoding of 6-bit values
        num_chars = (i + 2 < src_len) ? 4 : ((src_len - i) * 4) / 3 + 1;
        for( j = 0; j < num_chars; j++ )
        {
            dest[dest_len++] = s_ascii_encoding[txt[j]];
        }
    }

    dest[dest_len] = '\0';

    return dest_len;
}

/*
================
SV_RankAsciiDecode
 
Decodes src_len characters of ASCII text from the src buffer, stores 
the binary result in the dest buffer.
 
The dest buffer must be at least (src_len * 3) / 4 bytes in length.
 
Returns the length of the binary result, or zero for invalid input.
================
*/
static int SV_RankAsciiDecode( unsigned char* dest, const char* src,
                               int src_len )
{
    static unsigned char	s_inverse_encoding[256];
    static char				s_init = 0;

    unsigned char	bin[3];
    unsigned char	txt[4];
    int				dest_len = 0;
    int				i;
    int				j;
    int				num_bytes;

    assert( dest != NULL );
    assert( src != NULL );

    if( !s_init )
    {
        // initialize lookup table for decoding
        memset( s_inverse_encoding, 255, sizeof(s_inverse_encoding) );
        for( i = 0; i < 64; i++ )
        {
            s_inverse_encoding[s_ascii_encoding[i]] = i;
        }
        s_init = 1;
    }

    for( i = 0; i < src_len; i += 4 )
    {
        // read four characters of input, decode them to 6-bit values
        for( j = 0; j < 4; j++ )
        {
            txt[j] = (i + j < src_len) ? s_inverse_encoding[src[i + j]] : 0;
            if (txt[j] == 255)
            {
                return 0; // invalid input character
            }
        }

        // get three bytes from four 6-bit values
        bin[0] = (txt[0] << 2) | (txt[1] >> 4);
        bin[1] = (txt[1] << 4) | (txt[2] >> 2);
        bin[2] = (txt[2] << 6) | txt[3];

        // store binary data
        num_bytes = (i + 3 < src_len) ? 3 : ((src_len - i) * 3) / 4;
        for( j = 0; j < num_bytes; j++ )
        {
            dest[dest_len++] = bin[j];
        }
    }

    return dest_len;
}

/*
================
SV_RankEncodeGameID
================
*/
static void SV_RankEncodeGameID( uint64_t game_id, char* result,
                                 int len )
{
    assert( result != NULL );

    if( len < ( ( sizeof(game_id) * 4) / 3 + 2) )
    {
        Com_DPrintf( "SV_RankEncodeGameID: result buffer too small\n" );
        result[0] = '\0';
    }
    else
    {
        qint64 gameid = LittleLong64(*(qint64*)&game_id);
        SV_RankAsciiEncode( result, (unsigned char*)&gameid,
                            sizeof(qint64) );
    }
}

/*
================
SV_RankDecodePlayerID
================
*/
static uint64_t SV_RankDecodePlayerID( const char* string )
{
    unsigned char	buffer[9];
    int len;
    qint64	player_id;

    assert( string != NULL );

    len = strlen (string) ;
    Com_DPrintf( "SV_RankDecodePlayerID: string length %d\n",len );
    SV_RankAsciiDecode( buffer, string, len );
    player_id = LittleLong64(*(qint64*)buffer);
    return *(uint64_t*)&player_id;
}

/*
================
SV_RankDecodePlayerKey
================
*/
static void SV_RankDecodePlayerKey( const char* string, GR_PLAYER_TOKEN key )
{
    unsigned char	buffer[1400];
    int len;
    assert( string != NULL );

    len = strlen (string) ;
    Com_DPrintf( "SV_RankDecodePlayerKey: string length %d\n",len );

    memset(key,0,sizeof(GR_PLAYER_TOKEN));
    memset(buffer,0,sizeof(buffer));
    memcpy( key, buffer, SV_RankAsciiDecode( buffer, string, len ) );
}

/*
================
SV_RankStatusString
================
*/
static char* SV_RankStatusString( GR_STATUS status )
{
    switch( status )
    {
    case GR_STATUS_OK:
        return "GR_STATUS_OK";
    case GR_STATUS_ERROR:
        return "GR_STATUS_ERROR";
    case GR_STATUS_BADPARAMS:
        return "GR_STATUS_BADPARAMS";
    case GR_STATUS_NETWORK:
        return "GR_STATUS_NETWORK";
    case GR_STATUS_NOUSER:
        return "GR_STATUS_NOUSER";
    case GR_STATUS_BADPASSWORD:
        return "GR_STATUS_BADPASSWORD";
    case GR_STATUS_BADGAME:
        return "GR_STATUS_BADGAME";
    case GR_STATUS_PENDING:
        return "GR_STATUS_PENDING";
    case GR_STATUS_BADDOMAIN:
        return "GR_STATUS_BADDOMAIN";
    case GR_STATUS_DOMAINLOCK:
        return "GR_STATUS_DOMAINLOCK";
    case GR_STATUS_TIMEOUT:
        return "GR_STATUS_TIMEOUT";
    case GR_STATUS_INVALIDUSER:
        return "GR_STATUS_INVALIDUSER";
    case GR_STATUS_INVALIDCONTEXT:
        return "GR_STATUS_INVALIDCONTEXT";
    default:
        return "(UNKNOWN)";
    }
}

/*
================
SV_RankError
================
*/
static void SV_RankError( const char* fmt, ... )
{
    va_list	arg_ptr;
    char	text[1024];

    va_start( arg_ptr, fmt );
    vsprintf( text, fmt, arg_ptr );
    va_end( arg_ptr );

    Com_DPrintf( "****************************************\n" );
    Com_DPrintf( "SV_RankError: %s\n", text );
    Com_DPrintf( "****************************************\n" );

    s_rankings_active = qfalse;
    Cvar_Set( "sv_rankingsActive", "0" );
    // FIXME - attempt clean shutdown?
}

